import 'reflect-metadata'
import express, { Express, Request as ExpressRequest, Response as ExpressResponse, NextFunction } from 'express'
import { Logger } from './logger'
import path from 'path'
import { INJECTED_TOKEN, DESIGN_PARAMTYPES } from '../common'

// 扩展 Express.Request 接口
declare global {
  namespace Express {
    interface Request {
      user?: {
        name: string;
        role: string;
      }
    }
  }
}

/**
 * 创建 nest app 应用\
 */
export class NestApplication {
  private readonly app: Express = express() // 创建 express app 应用实例 提供 HTTP 相关服务
  // 保存所有的提供者 key 是 token令牌 值是提供者实例(类提供者实例)或提供者的值(工厂函数提供者或值提供者)
  private readonly providerInstances = new Map()
  // 记录每个模块里面有哪些 provider 提供者的 token
  // key是模块类 值是一个 Set 里面保存了该模块对应的提供者token
  private readonly moduleProviders = new Map() 
  private readonly globalProviders = new Set() // 记录全局模块中暴露的提供者 所有的模块内都可以使用的提供者的 token
  /**
   * 使用根模块来初始化项目
   * @param module 根模块
   */
  constructor(protected readonly module) {
    // 使用 express 内置的中间件来解析请求体中的JSON数据
    this.app.use(express.json())
    // 使用 express 内置的中间件来解析请求体中的URL application/x-www-form-urlencoded 编码数据
    this.app.use(express.urlencoded())

    this.app.use((req, res, next) => {
      req.user = {
        name: 'zs',
        role: 'admin'
      }
      next()
    })
    this.initProviders() // 处理根模块的提供者
  }

  initProviders() {
    // 获取根模块导入的模块类组成的数组 如果没有导入则为空数组
    const imports = Reflect.getMetadata('imports', this.module) ?? []
    for (const importModule of imports) { // 遍历数组 获取每个模块类
      this.registerProvidersFromModule(importModule, this.module) // 获取导入模块暴露的提供者
    }
    // 获取根模块类 providers 元数据 值是根模块类自身的提供者 ( 如果没有提供者则默认是一个空数组 )
    const providers = Reflect.getMetadata('providers', this.module) ?? []
    for (const provider of providers) { // 遍历根模块提供者数组 获取每个根模块提供者
      this.addProvider(provider, this.module) // 将根模块的提供者添加到 Map 中
    }
  }

  // 通过模块类树注册提供者
  // 根模块引入了 A模块 和 B模块 并且 A模块暴露了 AService 提供者 该提供者对 根模块 + A模块都是可见的
  // module是导入模块 ...parentModules 是祖先模块 需要把导入模块中暴露的提供者关联到导入模块+祖先模块
  // 如果 module导入模块暴露的是一个模块 则需要处理该模块的提供者 递归
  private registerProvidersFromModule(module, ...parentModules) {
    // 从模块类中取出 global 元数据 ( true代表全局模块 如果取不到元数据对应的值则该模块类不是一个全局模块 )
    const global = Reflect.getMetadata('global', module) 
    // 获取导入模块类的 providers 元数据 值是导入模块类自己的提供者组成的数组 ( 如果没有则默认是一个空数组 )
    const importedProviders = Reflect.getMetadata('providers', module) ?? []
    // 获取导入模块类的 exports 元数据 值是导入模块暴露的提供者和模块 ( 如果没有则默认是一个空数组 )
    const exports = Reflect.getMetadata('exports', module) ?? []
    for (const exportToken of exports) { // 遍历获取导入模块类暴露的每个提供者或模块
      if (this.isModule(exportToken)) { // 导入模块暴露的是一个模块
        this.registerProvidersFromModule(exportToken, module, ...parentModules) // 递归 暴露的是一个模块
      } else { // 导入模块暴露的是一个提供者
        // 从 导入模块 的提供者中基于 exportToken 获取 导入模块 对应的提供者
        const provider = importedProviders.find(provider => {
          return provider === exportToken || provider.provide === exportToken
        })
        if (provider) { // 
          [module, ...parentModules].forEach((m) => {
            this.addProvider(provider, m, global) // 导入模块暴露的提供者保存到 Map 中
          })
        }
      }
    }
  }

  // 判断 exportToken 是一个模块类
  private isModule(exportToken) {
    return exportToken instanceof Function && Reflect.getMetadata('isModule', exportToken)
  }

  // 处理提供者 将提供者token令牌与所属模块关联保存起来 ( 模块之间的提供者隔离 ) + 全局模块暴露的提供者在所有模块或提供者内都可用
  // 保存提供者 ( 类提供者保存实例 值提供者保存值 工厂函数提供者保存工厂函数调用后的值 )
  addProvider(provider, module, global = false) {
    // 从 this.moduleProviders 里面获取 module 模块类对应的 Set 如果没有则默认为一个空 Set
    // 如果是全局模块则获取 this.globalProviders 否则获取 this.moduleProviders 中对应的 Set
    const providers = global ? this.globalProviders : (this.moduleProviders.get(module) || new Set())
    // module 如果不是全局模块 并且 this.moduleProviders 中没有 module 模块类关联的 Set
    if (!global && !this.moduleProviders.has(module)) {
      // 把 模块类 和关联的 Set 提供者 token令牌 关联起来并保存到 this.moduleProviders 中
      this.moduleProviders.set(module, providers)
    }

    let injectToken = provider.provide ?? provider // 获取提供者的 token令牌
    if (this.providerInstances.has(injectToken)) { // this.providerInstances 提供者实例池中已经存在则不需要再次创建提供者
      return providers.add(injectToken) // 直接在对应 Set 里面保存这个 token令牌
    }

    if (provider.provide && provider.useClass) { // 类提供者
      const clazz = provider.useClass // 获取类提供者
      const dependencies = this.resolveDependencies(clazz) // 获取类提供者的依赖
      const classInstance = new clazz(...dependencies) // 实例化类提供者
      this.providerInstances.set(provider.provide, classInstance) // token令牌 和 类实例 以 key/value 的形式保存到 Map 中
      providers.add(provider.provide) // 将 token令牌 保存到 Set 中
    } else if (provider.provide && provider.useValue) { // 值提供者
      this.providerInstances.set(provider.provide, provider.useValue) // token令牌 和 值 以 key/value 的形式保存到 Map 中
      providers.add(provider.provide) // 将 token令牌 保存到 Set 中
    } else if (provider.provide && provider.useFactory) { // 工厂提供者
      const inject = provider.inject ?? [] // 工厂提供者依赖的提供者组成的数组 默认是一个空数组
      // 基于 token令牌 获取对应的提供者
      const injectValue = inject.map((injectToken) => this.getProviderByToken(injectToken, module))
      const value = provider.useFactory(...injectValue) // 调用工厂函数 获取返回的值
      this.providerInstances.set(provider.provide, value) // token令牌 和 值 以 key/value 的形式保存到 Map 中
      providers.add(provider.provide) // 将 token令牌 保存到 Set 中
    } else { // 类提供者 简写形式
      const dependencies = this.resolveDependencies(provider) // 获取类提供者的依赖
      const classInstance = new provider(...dependencies) // 实例化类提供者
      this.providerInstances.set(provider, classInstance) // 类提供者 和 类提供者实例 以 key/value 的形式保存到 Map 中
      providers.add(provider) // 将 token令牌 保存到 Set 中
    }
  }

  use(middleware) {
    this.app.use(middleware) // 使用中间件
  }

  /**
   * 基于 token 令牌返回对应的提供者
   * @param injectToken 提供者的 token 令牌
   * @returns 提供者
   */
  private getProviderByToken = (injectToken, module) => {
    // 判断 module 模块类有没有对应的 injectToken
    if (this.moduleProviders.get(module)?.has(injectToken) || this.globalProviders.has(injectToken)) {
      // 有 则从 this.providerInstances 中基于 injectToken 取出对应的提供者 或 全局模块中的提供者
      return this.providerInstances.get(injectToken)
    } else {
      return null
    }
  }

  /**
   * 处理控制器类构造函数或提供者类构造函数参数的 token 令牌依赖
   * @param Clazz 控制器类或提供者类
   */
  private resolveDependencies(Clazz) {
    // 从类上取出 INJECTED_TOKEN 元数据 ( 有可能没有任何依赖则使用空数组作为默认值 )
    // [ <1 empty item>, 'StringToken' ]
    const injectedTokens = Reflect.getMetadata(INJECTED_TOKEN, Clazz) ?? []
    // design:paramtypes 获取构造函数参数类型组成的数组 如果参数没有写类型则类型默认为 Object 
    // [ [class LoggerService], [class useValueService] ] 
    // 不是每一个控制器类或者提供者类都有 design:paramtypes 元数据对应的值 当没有值的时候给一个空数组作为默认值 ( 当类没有构造函数时元数据值为 undefined )
    const constructorParams = Reflect.getMetadata(DESIGN_PARAMTYPES, Clazz) ?? []
    // 遍历 design:paramtypes 构造函数参数类型组成的数组 param参数类型 index参数索引
    return constructorParams.map((param, index) => {
      // 从提供者或控制器类中获取 nestModule 元数据 其中 值是对应的 模块类
      const module = Reflect.getMetadata('nestModule', Clazz)
      // 优先基于索引从 injectedTokens 中取 token 令牌
      // 其次使用参数类型作为 token 令牌
      return this.getProviderByToken(injectedTokens[index] ?? param, module)
    })
  }

  /**
   * 配置初始化工作\
   * 取出模块里面的所有控制器 然后映射路径
   */
  async init() {
    // 获取根模块类中 controllers 元数据 ( 是一个包含相关控制器类的数组 ) 如果元数据的值没有则是 []
    const controllers = Reflect.getMetadata('controllers', this.module) || []
    Logger.log(`AppModule dependencies initialized `, 'InstanceLoader')
    // 遍历根模块中的所有控制器类
    for (const Controller of controllers) {
      const dependencies = this.resolveDependencies(Controller) // 解析出控制器类的依赖
      const controller = new Controller(...dependencies) // 实例化控制器类
      // 获取控制器类上的路径前缀 如果没有就使用 '/' 作为默认值
      const prefix = Reflect.getMetadata('prefix', Controller) || '/'
      Logger.log(`${Controller.name} {${prefix}}`, 'RoutesResolver')
      const controllerPrototype = Reflect.getPrototypeOf(controller) // 获取控制器实例的原型
      // 遍历控制器原型对象上( 不包含 Symbol 作为 key )的每个自有属性组成的数组 ['getHello', 'handleQuery', 'handleHandlers'...]
      for (const methodName of Object.getOwnPropertyNames(controllerPrototype)) {
        const method = controllerPrototype[methodName] // 获取控制器类原型上的方法 可能该方法是一个路由处理函数
        const httpMethod = Reflect.getMetadata('method', method) // 获取方法上的 method 元数据
        const pathMetadata = Reflect.getMetadata('path', method) // 获取方法上的 path 元数据
        const redirectUrl = Reflect.getMetadata('redirectUrl', method) // 获取方法上的重定向路径
        const redirectStatusCode = Reflect.getMetadata('redirectStatusCode', method) // 获取重定向状态码
        const statusCode = Reflect.getMetadata('statusCode', method) // 获取响应状态码
        const headers = Reflect.getMetadata('headers', method) ?? [] // 获取需要设置的响应头 默认值为空数组
        if (!httpMethod) continue; // 如果方法上没有 method 元数据 代表当前方法不是一个路由处理函数
        const routePath = path.posix.join('/', prefix, pathMetadata) // 拼接HTTP请求路径
        // 监听HTTP请求 this.app.get(请求路径)
        this.app[httpMethod.toLowerCase()](routePath, (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
          const args = this.resolveParams(controller, methodName, req, res, next)
          // 使用 apply 来执行路由处理函数时并指定 this 为 控制器实例对象 获取路由处理函数的返回值
          const result = method.apply(controller, args)
          // 判断是否需要重定向
          if (typeof redirectUrl === 'string') {
            if (typeof result?.url === 'string') { // 优先使用 HttpRedirectResponse 接口的返回值
              return res.redirect(result.statusCode, result.url)
            } else { // 其次使用 @Redirect() 装饰器指定的重定向路径和响应状态码
              return res.redirect(redirectStatusCode, redirectUrl)
            }
          }
          // 判断是否手动设置了响应状态码
          if (statusCode) {
            res.statusCode = statusCode // 使用手动设置的响应状态码
          } else if (httpMethod === 'POST') { // 默认情况下 POST 请求成功响应的状态码为 201
            res.statusCode = 201
          }

          // 使用了 @Header 装饰器设置响应头
          headers.forEach(({ name, value }) => {
            res.setHeader(name, value) // 根据响应头数组设置响应头
          })

          // 判断当前路由处理函数参数中是否使用了 @Res() 或 @Response() 或 @Next() 装饰器
          const responseMetadata = this.getResponseMetadata(controller, methodName)
          if (!responseMetadata || responseMetadata?.data?.passthrough) {

            // 没有使用 @Res() 或 @Response() 装饰器 或者 使用了 @Res() 或 @Response() 装饰器并配置了 {passthrough: true}
            // 默认情况下 nestjs 会使用 express 中间件里面的 res.send 方法来返回响应结果
            // res.send 方法支持 字符串 对象 数组 数字 布尔值 null undefined 
            res.send(result)
          }
        })
        Logger.log(`Mapped ${routePath}, ${httpMethod} route`, 'RoutesResolver')
      }
    }
    Logger.log(`Nest application successfully started`, 'InstanceLoader')
  }

  /**
   * 用于判断路由处理函数参数中是否使用了 @Res装饰器 或 @Response装饰器 或 @Next装饰器
   * @param controller 控制器类实例
   * @param methodName 方法名称
   */
  private getResponseMetadata(controller, methodName) {
    const paramsMetaData = Reflect.getMetadata(`params`, controller, methodName) ?? []
    return paramsMetaData.filter(Boolean).find(
      param =>
        param.key === 'Response' ||
        param.key === 'Res' ||
        param.key === 'Next'
    )
  }

  /**
   * 解析控制器类原型路由处理函数的参数
   * @param instance 控制器类实例
   * @param methodName 方法名
   * @param req 请求对象
   * @param res 响应对象
   * @param next 下一个中间件
   */
  private resolveParams(instance, methodName: string, req: ExpressRequest, res: ExpressResponse, next: NextFunction) {
    // 从控制器类实例上取 methodName 的 `params` 元数据发现没有 则从控制器类实例的原型上取 metadataName 的 `params` 元数据
    const paramsMetaData = Reflect.getMetadata(`params`, instance, methodName) ?? []
    // a.parameterIndex - b.parameterIndex 当a的参数索引小于b的参数索引则在前面 当a的参数索引大于b的参数索引时a在b的后面
    // [ { parameterIndex: 1, key: 'Request' }, { parameterIndex: 0, key: 'Req' } ] 排序前
    // [ { parameterIndex: 0, key: 'Req' }, { parameterIndex: 1, key: 'Request' } ] 排序后
    return paramsMetaData.map(paramMetaData => {
      // key 是参数装饰器名称 比如 'Request' 'Req' 'Query'
      // data 是使用参数装饰器工厂函数传递的参数
      const { key, data, factory } = paramMetaData
      const ctx = {
        switchToHttp: () => ({
          getRequest: () => req,
          getResponse: () => res,
          getNext: () => next
        })
      }
      switch (key) {
        case 'Request':
        case 'Req':
          return req;
        case 'Query':
          return data ? req.query[data] : req.query
        case 'Headers':
          return data ? req.headers[data] : req.headers
        case 'Session':
          return data ? req.session[data] : req.session
        case 'Ip':
          return req.ip
        case 'Param':
          return data ? req.params[data] : req.params
        case 'Body':
          // 依赖 express 中的 express.json() 处理请求体中的 JSON 数据
          // 依赖 express 中的 express.urlencoded() 处理请求体中的 URL application/x-www-form-urlencoded 编码数据
          return data ? req.body[data] : req.body
        // 使用 @Res() 或 @Response() 装饰器时需要手动管理如何响应 如果不手动响应则会被挂起
        case 'Response':
        case 'Res':
          return res
        case 'DecoratorFactory': // 自定义参数装饰器
          return factory(data, ctx);
        default: null
      }
    })
  }

  /**
   * 启动 HTTP 服务并监听端口
   * @param port 端口
   */
  async listen(port: number) {
    await this.init()
    // 使用 express 中的 listen 方法启动 http 服务监听端口
    this.app.listen(port, () => {
      Logger.log(`Application is running on http://localhost:${port}`, 'NestApplication')
    })
  }
}



/**
 * 在生成HTTP请求路径时 path.posix.join 调用时 传递的第一个参数永远都是 '/'
 * path.posix.join 可以将
 * path.posix.join('/', '/') 转换成 '/'
 * path.posix.join('/', '/abc') 转换成 '/abc'
 * path.posix.join('/', 'abc') 转换成 '/abc'
 * path.posix.join('/', '/', 'abc') 转换成 '/abc'
 * path.posix.join('/', '/', '/abc') 转换成 '/abc'
 * path.posix.join('/', ':id') 转换成 '/:id'
 * path.posix.join('/cat', ':id') 转换成 '/cat/:id'
 * path.posix.join('/cat', '/:id') 转换成 '/cat/:id'
 * path.posix.join('/cat////', '/:id///') 转换成 '/cat/:id/'
 */
